package com.commbizobj.money;

import java.io.Serializable;
import java.util.Currency;
import java.util.Locale;

/**
 * Implementation of Money.
 * 
 * Value refers to an integer of the smallest unit in that currency. e.g. 234 = 2.34 CNY, or 2.34 USD, or 234 JPY, or 234 KRW
 * CurrencyCode refers to ISO 4217 code. <a href="http://en.wikipedia.org/wiki/ISO_4217">see here</a>
 * 
 * @author Otacon
 *
 */
public class Money implements Cloneable, Serializable, Comparable<Money>{
    private static final long serialVersionUID = 4813245452456650172L;

    private Currency currency;
    private long value;
    
    private Money() {}
    
    /**
     * Static factory method used to create a new Money instance.
     * @param currencyCode The 3-letter ISO currency code.
     * @param value The price value in the smallest unit of the currency.
     * @return  New price instance created by the given parameters.
     */
    public static Money getInstanceByValue(String currencyCode, long value) {
        Money newInstance = new Money();
        newInstance.setCurrencyCode(currencyCode);
        newInstance.setValue(value);
        return newInstance;
    }
    
    /**
     * Static factory method used to create a new Money instance.
     * @param currencyCode The 3-letter ISO currency code.
     * @param value The price value expressed in decimal form.
     * @return  New price instance created by the given parameters.
     */
    public static Money getInstanceByDecimalValue(String currencyCode, double decimalValue) {
        Money newInstance = new Money();
        newInstance.setCurrencyCode(currencyCode);
        newInstance.setDecimalValue(decimalValue);
        return newInstance;
    }
    
    public String getCurrencyCode(){
        return (currency == null) ? null : currency.getCurrencyCode();
    }
        
    public void setCurrencyCode(String code){
        currency = Currency.getInstance(code);
    }
    
    private long getValue() {
        return value;
    }
    
    private void setValue(long value) {
        this.value = value;
    }
    
    public double getDecimalValue() {
        return getValue() / Math.pow(10,currency.getDefaultFractionDigits());
    }
    
    public void setDecimalValue(double decimalValue) {
        if(currency!=null){
            double targetValue = decimalValue * Math.pow(10,currency.getDefaultFractionDigits());
            setValue((long)targetValue);
        }
    }
    
    public String getDecimalValueFormatted() {
        StringBuffer sb = new StringBuffer("%.");
        sb.append(currency.getDefaultFractionDigits()).append('f');
        return String.format(sb.toString(), getDecimalValue());
    }
    
    // Arithmetic methods - Note: arithmetic does not affect either operands. The result will be a new instance.
    private void validateArithmeticArgument(Money p) throws IllegalArgumentException {
        if(!this.currency.equals(p.currency)) {
            throw new UnsupportedOperationException("Money operands must be of the same currency.");
        }
    }
    
    public Money add(Money p) throws IllegalArgumentException{
        validateArithmeticArgument(p);
        Money newValue = this.clone();
        newValue.setValue(value + p.value);
        return newValue;
    }
    
    public Money substract(Money p) throws IllegalArgumentException{
        validateArithmeticArgument(p);
        Money newValue = this.clone();
        newValue.setValue(value - p.value);
        return newValue;
    }
    
    public Money multiply(double factor) {
        if(factor < 0) {
            throw new IllegalArgumentException("Multiplication factor cannot be negative.");
        }
        Money newValue = this.clone();
        newValue.setValue((long)(value * factor));
        return newValue;
    }
    
    // Other util methods
    @Override
    public int compareTo(Money p) {
        validateArithmeticArgument(p);
        return (int)(value - p.value);
    }
    
    @Override
    public String toString() {
        return toString(Locale.getDefault());
    }
    
    public String toString(Locale locale) {
        return String.format("%s %s", currency.getSymbol(locale), getDecimalValueFormatted());
    }
    
    @Override
    public Money clone() {
        return Money.getInstanceByValue(currency.getCurrencyCode(), value);
    }
    
    @Override
    public boolean equals(Object p) {
        if(!(p instanceof Money)) {
            return false;
        }
        Money input = (Money) p;
        return input.currency.equals(currency) && input.value == value;
    }
    
    @Override
    public int hashCode() {
        return currency.hashCode() + (int) value;
    }
}
